Listes et ScrollViews
ListView : Affichage de listes scrollables
ListView basique
Le ListView affiche une liste scrollable de widgets. Idéal pour un petit nombre d'éléments.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ListView basique')),
body: ListView(
children: const [
ListTile(
leading: Icon(Icons.person),
title: Text('Alice'),
subtitle: Text('alice@example.com'),
),
ListTile(
leading: Icon(Icons.person),
title: Text('Bob'),
subtitle: Text('bob@example.com'),
),
ListTile(
leading: Icon(Icons.person),
title: Text('Charlie'),
subtitle: Text('charlie@example.com'),
),
],
),
),
);
}
}
ListView.builder : Construction dynamique
ListView.builder est plus performant pour les listes dynamiques car il ne construit que les éléments visibles.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ListView.builder')),
body: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text('Élément $index'),
subtitle: Text('Description de l\'élément $index'),
trailing: const Icon(Icons.arrow_forward),
onTap: () {
debugPrint('Élément $index cliqué');
},
);
},
),
),
);
}
}
ListView.separated : Avec séparateurs
Ajoute automatiquement des séparateurs entre les éléments.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ListView.separated')),
body: ListView.separated(
itemCount: 20,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
),
),
);
}
}
ListView avec données complexes
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class Person {
final String name;
final String email;
final String avatar;
const Person(this.name, this.email, this.avatar);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Liste avec données')),
body: PersonList(),
),
);
}
}
class PersonList extends StatelessWidget {
PersonList({super.key});
final List<Person> people = const [
Person('Alice', 'alice@example.com', 'A'),
Person('Bob', 'bob@example.com', 'B'),
Person('Charlie', 'charlie@example.com', 'C'),
];
Widget build(BuildContext context) {
return ListView.builder(
itemCount: people.length,
itemBuilder: (context, index) {
final person = people[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: Text(person.avatar, style: const TextStyle(fontSize: 32)),
title: Text(person.name),
subtitle: Text(person.email),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
// Logique de suppression
},
),
),
);
},
);
}
}
GridView : Affichage en grille
GridView.count : Grille avec nombre de colonnes fixe
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('GridView.count')),
body: GridView.count(
crossAxisCount: 3,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
padding: const EdgeInsets.all(10),
children: List.generate(20, (index) {
return Container(
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Text(
'Item $index',
style: const TextStyle(color: Colors.white),
),
),
);
}),
),
),
);
}
}
GridView.builder : Grille dynamique
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('GridView.builder')),
body: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.0,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
),
padding: const EdgeInsets.all(10),
itemCount: 50,
itemBuilder: (context, index) {
return Card(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.image, size: 50, color: Colors.blue),
const SizedBox(height: 8),
Text('Image $index'),
],
),
);
},
),
),
);
}
}
GridView.extent : Taille maximale des éléments
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('GridView.extent')),
body: GridView.extent(
maxCrossAxisExtent: 150,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
padding: const EdgeInsets.all(10),
children: List.generate(20, (index) {
return Container(
color: Colors.teal,
child: Center(child: Text('$index')),
);
}),
),
),
);
}
}
Widgets de liste avancés
Dismissible : Glisser pour supprimer
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: DismissibleExample(),
);
}
}
class DismissibleExample extends StatefulWidget {
const DismissibleExample({super.key});
State<DismissibleExample> createState() => _DismissibleExampleState();
}
class _DismissibleExampleState extends State<DismissibleExample> {
final List<String> items = List.generate(20, (index) => 'Item ${index + 1}');
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Dismissible')),
body: ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return Dismissible(
key: Key(item),
background: Container(
color: Colors.red,
alignment: Alignment.centerRight,
padding: const EdgeInsets.only(right: 20),
child: const Icon(Icons.delete, color: Colors.white),
),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
setState(() {
items.removeAt(index);
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('$item supprimé')),
);
},
child: ListTile(
title: Text(item),
),
);
},
),
);
}
}
ReorderableListView : Réorganiser les éléments
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
home: ReorderableExample(),
);
}
}
class ReorderableExample extends StatefulWidget {
const ReorderableExample({super.key});
State<ReorderableExample> createState() => _ReorderableExampleState();
}
class _ReorderableExampleState extends State<ReorderableExample> {
final List<String> items = List.generate(10, (index) => 'Item ${index + 1}');
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ReorderableListView')),
body: ReorderableListView(
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final item = items.removeAt(oldIndex);
items.insert(newIndex, item);
});
},
children: items.map((item) {
return ListTile(
key: Key(item),
title: Text(item),
leading: const Icon(Icons.drag_handle),
);
}).toList(),
),
);
}
}
SingleChildScrollView : Scroll simple
Pour du contenu scrollable qui n'est pas une liste.
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('SingleChildScrollView')),
body: SingleChildScrollView(
child: Column(
children: const [
SizedBox(height: 200, child: ColoredBox(color: Colors.red)),
SizedBox(height: 200, child: ColoredBox(color: Colors.blue)),
SizedBox(height: 200, child: ColoredBox(color: Colors.green)),
SizedBox(height: 200, child: ColoredBox(color: Colors.yellow)),
],
),
),
),
);
}
}
Scroll horizontal
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Scroll horizontal')),
body: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.generate(10, (index) {
return Container(
width: 150,
height: 150,
margin: const EdgeInsets.all(8),
color: Colors.primaries[index % Colors.primaries.length],
child: Center(child: Text('$index')),
);
}),
),
),
),
);
}
}
ListTile et Card
ListTile personnalisé
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ListTile personnalisé')),
body: ListView(
children: [
ListTile(
leading: const CircleAvatar(
backgroundImage: NetworkImage('https://example.com/avatar.jpg'),
),
title: const Text('John Doe'),
subtitle: const Text('En ligne il y a 5 minutes'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.phone),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.message),
onPressed: () {},
),
],
),
onTap: () {
debugPrint('ListTile cliqué');
},
),
],
),
),
);
}
}
Card avec contenu complexe
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Card complexe')),
body: ListView(
children: [
Card(
elevation: 4,
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const CircleAvatar(
child: Icon(Icons.person),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'Titre de la carte',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text('Sous-titre'),
],
),
],
),
const SizedBox(height: 16),
const Text(
'Contenu de la carte avec plus de détails...',
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {},
child: const Text('ANNULER'),
),
TextButton(
onPressed: () {},
child: const Text('OK'),
),
],
),
],
),
),
),
],
),
),
);
}
}
ExpansionTile : Liste extensible
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ExpansionTile')),
body: ListView(
children: const [
ExpansionTile(
leading: Icon(Icons.folder),
title: Text('Dossier'),
subtitle: Text('3 éléments'),
children: [
ListTile(
leading: Icon(Icons.file_present),
title: Text('Document 1'),
),
ListTile(
leading: Icon(Icons.file_present),
title: Text('Document 2'),
),
ListTile(
leading: Icon(Icons.file_present),
title: Text('Document 3'),
),
],
),
],
),
),
);
}
}
Optimisation des performances
Utiliser const
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Optimisation: const')),
body: ListView.builder(
itemCount: 30,
itemBuilder: (context, index) {
return const ListTile(
leading: Icon(Icons.star), // const pour optimisation
title: Text('Item'),
);
},
),
),
);
}
}
Limiter la hauteur des items
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Optimisation: itemExtent')),
body: ListView.builder(
itemCount: 50,
itemExtent: 80, // Hauteur fixe pour meilleures performances
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
),
),
);
}
}
Bonnes pratiques
- ListView.builder pour listes dynamiques (+ de 10 éléments)
- ListView simple pour petites listes statiques
- Dismissible pour actions de suppression intuitives
- Key unique pour chaque élément dans listes dynamiques
- const autant que possible pour optimiser les performances
- itemExtent ou prototypeItem pour hauteurs fixes